Unreal GAS系统使用学习
前置知识
简介
Gameplay Ability System,简称(GAS)是一个健壮的、高度可扩展的、gameplay框架,通常用于构建RPG、MOBA等游戏的完整战斗逻辑框架。通过GAS,可以快速地制作游戏中的主动/被动技能、各种效果buff、计算属性伤害、处理玩家各种战斗状态逻辑。
GAS试图将机制提取为通用的游戏设计模式,并提供框架来解决常见的Gameplay实现问题,同时使上下文因项目而异。
GAS提供了哪些功能?
- 实现了带有消耗和冷却功能的角色技能
- 处理数值属性(生命、魔法、攻击力、防御力)
- 应用状态效果(击飞、着火、眩晕)
- 应用游戏标签(GameplayTags)
- 生成特效和音效
- 完整的网络复制、预测功能
适合使用GAS的项目
C++项目,开发人员有充足的C++开发经验
使用Dedicated Server专用服务器的联机游戏(单纯的游戏逻辑服务端,与Dedicated Server相对的是Listen Server,其中一位玩家充当服务器,容易出现“主机优势”)
项目有大量且复杂的技能逻辑设计需求
GAS的主要优势
- 网络复制(Network Replication): 不必担心你的属性或减益效果得不到妥善地应用或复制。GAS会为你处理内部逻辑。
- 模块化(Modularity): 添加或更改游戏机制通常与实现和赋予新技能一样简单。通过将Gameplay功能分解为单独的资产,技能系统可以在完全不同的游戏对象或机制之间提供通用的通信层。例如,生命值(Health)可以划分到自己的属性集,并通过来自各种系统的Gameplay效果进行交互。
- 快速迭代(Fast iteration): GAS可以轻松更改单个游戏规则,而无需修改整个系统。用于计算游戏的数据源可以轻松交换,并且可以从相应的Gameplay效果中修改动作效果。
我为什么学习GAS
编写样板代码通常易出错且耗时,尤其是对于多人游戏而言。例如,你不希望花费大量时间来确保你的生命(Health)值正确复制,或者在你决定具有相同行为的能量(Energy)值时,复制相同的代码行。
GAS通过提供尽可能实现常见Gameplay功能的基础来解决这些问题,同时保持机制中立。GAS并非强制使用诸如生命值(Health)、弹药(Ammo)、近战攻击(Melee Attack)或毒药减益(Poison Debuff)之类的概念,而是提供了能够定义、复制和使用 属性(Attributes) 、 技能(Abilities) 和 效果(Effects) 的工具,然后你可以将这些工具专用于满足给定Gameplay机制的需求。
于我而言:公司的项目需要使用到联网的游戏服务。所以你会在我的笔记中看到一些斜体的备注,这些备注意味着对我自己项目的备注。可以忽略。
GAS的组件
ASC:who谁能放技能?
Ability System Component(ASC)是整个 GAS 的基础组件。ASC 本质上是一个 UActorComponent,用于处理整个框架下的交互逻辑,包括使用技能(GameplayAbility)、包含属性(AttributeSet)、处理各种效果(GameplayEffect)。所有需要应用 GAS 的对象(Actor),都必须拥有 GAS 组件。(即所有有技能交互的角色都是ASC)。
- ASC是一种角色组件,负责和GA、GE、AS打交道。
- 一般只放在Character or PlayerState上。
- 拥有 ASC 的 Actor 被称为 ASC 的 OwnerActor,ASC 实际作用的 Actor 叫做 AvatarActor。
GA:How:技能的逻辑?
Gameplay Ability(GA)标识了游戏中一个对象(Actor)可以做的行为或技能。它代表角色可以执行的任何技能或行为。
能力(Ability)可以是普通攻击或者吟唱技能,可以是角色被击飞倒地,还可以是使用某种道具,交互某个物件,甚至跳跃、飞行等角色行为也可以是Ability。
Ability可以被赋予对象或从对象的ASC中移除,对象同时可以激活多个GameplayAbility。
- 基本的移动输入、UI交互行为则不能或不建议通过GA来实现。
- 通过蓝图继承GamePlay Ability来实现
GA的执行流程
- TryActivateAbility :首先判断一下能不能执行。
- ActivateAbility:执行一套技能(有技能动画,比如发射一个火焰)。
- CommitAbility:技能释放了成功了,要扣蓝。
- Start AbilityTask * 2:技能执行中…技能执行结束。然后应用对应效果。
其中ActivateAbility和End Ability是类似于BeginPlay和EndPlay这样的函数,在蓝图中调用。
蓝图中的设置
主要选项解释
常见蓝图
- EndAbility 结束这个能力
高级设置
Replication Policy(复制策略): 决定该Ability的激活、执行等信息是否在网络上传播。常见选项有:
- Not Replicated:仅本地执行,不同步到其他客户端/服务器。
- Replicated:在服务器和客户端之间同步。
Instancing Policy(实例化策略): 决定Ability的实例化方式。
- Non-Instanced:所有激活者共享同一个Ability对象,适合无状态逻辑。
- Instanced Per Actor:每个拥有者有独立实例,适合有状态逻辑。
- Instanced Per Execution:每次激活都新建实例,适合需要隔离状态的复杂技能。
Server Respects Remote Ability Cancellation(服务器尊重远端取消):勾选后,服务器会响应客户端请求取消Ability的操作。适合需要客户端主动取消技能的场景。
Retrigger Instanced Ability(可重复触发实例化Ability):允许Ability在已激活时再次被激活(会重新实例化),适合可叠加或可重复释放的技能。
Net Execution Policy(网络执行策略):决定Ability的激活和执行在哪端进行。
- Local Predicted:客户端预测执行,服务器校验。
- Local Only:仅本地执行。
- Server Only:仅服务器执行。
- Server Initiated:服务器发起,客户端可参与。
Net Security Policy(网络安全策略):控制Ability的安全等级,防止被恶意客户端滥用。
- Client Or Server:客户端和服务器都可激活。
- Server Only Execution:只有服务器可激活。
- Server Only Execution and Data:只有服务器可激活且数据仅在服务器。
GA要做的事情
一般GA要做的事有:
- 设置GA的Tag、CD、Cost等属性。
- 获取必要信息,主要通过Get Actor Info。如果是通过Event调用的GA(使用Activate Ability From Event节点作为输入),还可以通过Gameplay Event Data获取。
- 编写逻辑,如播放动画、应用GE、应用冲量等。
- 一定不要忘了EndAbility。
GA的调用
GA的调用分成了主动调用(释放技能)和被动调用(挨打)两类,下面依次介绍不同的调用方法。
主动调用
在蓝图中主要有by Class和by Tag 两种调用方法。
byClass一次只能Activate一个GA,byTag可以Activate任意多个GA,配合Tag容器使用。
如果使用EnhancedInputAction插件来管理输入,要注意在某些设置下trigger会每帧都进行输出(本人测试环境为4.27,UE5似乎有一些改动。
1.62GB
古代山谷项目就使用了新版输入插件和GAS系统,可以看一看实现方法)。
只要能获取ASC,就可以在任何地方调用GA,比如行为树Task蓝图,甚至在GA蓝图中调用其他GA。
被动调用
Trigger可以理解为一个Tag,当ASC组件收到一个Trigger时,就会自动调用所有拥有该Trigger的GA。
Trigger的Tag在GA的details面板中设置。
Trigger的触发方式有三种,分别是:
- Gameplay Event: 当Owner收到一个带有Tag的Gameplay Event(不是Gameplay Effect的GE!)时调用一次GA,此时Owner不会拥有对应的Tag。
- Owner Tag Added: 当Owner获取对应Tag的时候调用一次GA。
- Owner Tag Present: 当Owner拥有Tag时调用GA,失去Tag时移除。
一般使用第一种方法,并配合SendGameplayEventToActor节点使用,如下图所示。(这张图是很久以前截的,Tag建议以Event开头)
受击效果的例子,发送一个Tag为Hit的Event给碰撞检测到的Actor
使用Gameplay Event调用的好处是,可以传入数据(Payload),是除了Get Actor Info外的另一种信息传递方法。
此时应该删除ActiveAbility节点,转而使用ActivateAbilityFromEvent事件。(不要通过在左上角重载函数的方式,右键空白处搜索才是对的)
GE:What技能改变的属性?
Gameplay Effect(GE)是Ability对自己或他人产生影响的途径。
GE通常可以被理解为我们游戏中的buff。比如增益/减益效果(修改属性)。
但是GAS中的GE也更加广义,释放技能时候的伤害结算,施加特殊效果的控制、霸体效果(修改GameplayTag)都是通过GE来实现的。
- GE只是一个可配置的*数据表 *,不可以添加逻辑。开发者创建一个UGameplayEffect的派生蓝图,就可以根据需求制作想要的效果。
- GE是纯蓝图
GT:if技能改变的条件?
FGameplayTags是一种层级标签,如Parent.Child.GrandChild。
通过GameplayTagManager进行注册。
替代了原来的Bool,或Enum的结构,可以在玩法设计中更高效的标记对象的行为或状态(比如一个灼烧效果,GE完成之后,便可以取消标签)。
我理解是一个在Actor下的Json。
- Tag的层级关系也需要合理设计,到了后期修改成本比较大。
Attribute Set
负责定义和持有属性,并且管理属性的变化,包括网络同步。
需要在Actor中被添加为成员变量,并注册到ASC(C++)。
一个ASC可以拥有一个或多个(不同的)AttributeSet,因此可以角色共享一个很大的Attribute Set,也可以每个角色按需添加Attribute Set。
可以在属性变化前(PreAttributeChange)后(PostGameplayEffectExecute)处理相关逻辑,可以通过委托的方式绑定属性变化。
Player State
在 GAS 系统中,ASC 可以选择挂载在 PlayerState 上而不是 Character 上。这样做的好处是:
- 持久性:当角色死亡或重生时,PlayerState 依然存在,技能和属性数据不会丢失
- 网络复制:PlayerState 天然支持网络复制,适合多人游戏场景
- UI 访问:UI 可以更方便地访问玩家的技能和属性信息,无需依赖具体的 Character 实例
- 观察者模式:即使玩家处于观察者状态(没有 Character),也能保持技能系统的状态
常见的做法是:
- 玩家控制的角色:ASC 放在 PlayerState 上
- AI 控制的角色:ASC 直接放在 Character 上
组件总结
Visual:技能的视觉效果?GameplayCue
Async:技能的长时行动?(异步)GameplayTask
Send:技能的消息事件?GamePplayEvent
这俩是同一个东西
基础使用
开启插件
- C++项目,安装Gameplay Ability插件。
- 在Tools菜单下面有GameplayCue Editor有了,就说明开启成功了。
- 在新建的蓝图的页面可以看到GamePlay相关的东西
使用Rider快速添加Unreal类
通过这里创建一个Character。可以快速拥有一些头文件。
ASC组件架构
你需要自己创建一个继承自ACharacter的C++类。
之后想用蓝图,就从这个自定义的C++Character类派生就可以了。
写一个Base GAS ACharacter文件
1 | PublicDependencyModuleNames.AddRange(new string[] |
在Build文件里添加如下的内容GameplayAbilities、GameplayTags、GameplayTasks。
在头文件里面添加一个AbilitySystemComponent,直接在角色上挂载 AbilitySystemComponent(ASC),可以适配多人游戏。
1 | TObjectPtr<UTCGAbilitySystemComponent> AbilitySystemComponent; |
在.cpp中构造函数部分实例化AbilitySystemComponent(ASC)。
1 | //实例化ASC |
角色类继承IAbilitySystemInterface接口,并实现GetASC函数
1 |
|
在CPP中实现
1 | UAbilitySystemComponent* ACharacterBase::GetAbilitySystemComponent() const |
👆这里的return即使写为null,也可能可能调用
这个在项目中的路径在:Source/TCG_AwesomeLive/CharacterSystem/TCGCharacterBase.h
把GA数组加入到Actor中
可以将UE原生的UGameplayAbility直接加入到需要Ability的Actor中(也是加到这个Base GAS ACharacter文件中)
1 | UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Test|Abilities") |
在C++中赋予GA:我们可以将我们所有的技能储存在一张DA表之中,然后从DA表里读取数据,并使用AbilitySystemComponent->GiveAbility将数据赋予ASC。
1 | // 添加能力 |
这样我们就可以在继承了Base Character的子类中,看到Ability的选项了。
在官方的Lyra案例里面使用AbilitySet来储存一类Ability,通过加载来实现赋予。参看下文Unreal官方案例“赋予GA”部分。
Unreal官方案例
UE中有一个非常不错的官方示例Lyra里面展示了一个完整的GAS系统。
项目里的设置
这个项目里开发者设置了一些调整选项,可以设定一些debug的参数。
比如子弹射击停留时间,生成的bot的数量。
项目蓝图常见前缀标识
WID_
全称可理解为 Weapon Item Definition。
用途:武器类的“物品定义”蓝图资产,继承自 ULyraInventoryItemDefinition(你在代码里看到的基类)。
ID_
可理解为(Generic)Item Definition。
用途:非武器的一般物品定义(比如消耗品、弹药、包裹、可堆叠资源等)也继承 ULyraInventoryItemDefinition,用 ID_ 前缀区分与 WID_(武器专用)资产。
B_
B_ 前缀在 Lyra 中通常代表一个 Blueprint 类资产(等同很多团队常用的 BP_,Lyra 项目里取了更短的 B_)。
源码解析
- 这一部分我主要参考的是这里加上了一些我自己的补充。整体结构采用了原本的结构,但是有我自己的删减,可以对照地看。
初始化Ability System
创建ASC
ASC用于总体控制GAS功能,可以挂在Character上或者PlayerState上,对于角色会死亡重生的场景,挂在PlayerState上更合适,不会因Character销毁而丢失数据。因此Lyra选择把ASC挂在了PlayerState上,这样还有额外作用,就是按Tab键显示计分板这种战斗无关的功能,也可以用GA实现,以往的习惯都是硬编码。
ULyraAbilitySystemComponent本身是一个Manager,与数据无关,所以PlayerState直接New了一个对象即可:
Source/LyraGame/GameModes/LyraGameState.h/cpp
1 | //声明 |
ASC里面也会记录Actor
Plugins/Runtime/GameplayAbilities/Source/GameplayAbilities/Public/AbilitySystemComponent.h
1 | private: |
ASC也会记录关联Actor的信息:
OwnerActor挂在哪个Actor上,这里是PlayerState;
AvatarActor是使用Ability实体,这里是Character。
这两个属性是ASC经常要获取的变量,除此之外,还有多个与Actor关联的属性会被高频读取,为了提高读取性能,ASC索性把它们复制了一份,集中存储在AbilityActorInfo变量中,包括了PlayerController,SkeletalMeshComponent,AnimInstance,MovementComponent,AffectedAnimInstanceTag。
赋予GA(前建立Experience做数据衔接)
创建完ASC后,要往这个容器里赋予GA了,表示Actor有这些能力。
Lyra工程高度模块化,配置比较分散,使用AbilitySet来储存一类Ability,通过加载来实现自动化配置和赋予GA。
首先创建一个Experience,首先什么是Experience?
定义角色: Experience(通过 ULyraExperienceDefinition)把一组运行时行为、资源和规则打包成一个可切换的体验(例如不同模式、地图或玩法配置)。
主要用途:在游戏启动或切换体验时告诉引擎“要启用哪些 GameFeatures、默认的 Pawn 数据、以及需要执行哪些动作/赋能”
原因
- 统一配置与可切换性:把调表(技能、效果、默认 pawn 等)放到 Experience 里,便于在不同玩法/模式间切换和复用,不用把能力写死在代码里。
- 数据驱动与延迟加载:Experience 可以在运行时启/停 GameFeatures、注入资源,再把需要的能力/效果批量下发。
- 作用域与生命周期可控:通过 Experience 可以在启用时授予、停用时撤销,方便整体管理和热加载。
1 | UCLASS(BlueprintType, Const) |
如图Lyra的结构。
继承自ULyraExperienceDefinition的:B_BasicShooterTestB_BasicShooterTest这个数据类里的Action分组里面,有需要授予的GA的DA表。
比如:**AbilitySet_Elimination(下面那个) 和 HeroData_ShooterGame里面的AbilitySet_**ShooterHero。
- 首先看AbilitySet_Elimination DA表(它继承自LyraAbilitySet)。
打开这个AbilitySet_Elimination,里面包含两个GA。
GA_ShowLeaderboard_TDM用于按Tab显示计分板。
GA_AutoRespawn用于死亡复活,这两个GA与Character战斗无关,在没用Character时也能执行。
- 然后看AbilitySet_ShooterPistol DA表(储存在总表的HeroData_ShooterGame里面 它继承自ULyraPawnData)
包含Character相关的GA,数量较多。比如GA_Hero_Jump实现了跳跃,GA_Emote实现了跳舞。
- 然后还有一个AbilitySet_ShooterPisto
它不是定义在B_BasicShooterTestB_BasicShooterTest这张大表里面的。
而是通过WID_Pistol被具体的蓝图调用。
然后对每个GA,调用UAbilitySystemComponent::GiveAbility函数进行赋予。
会在很多地方调用。
1 | FGameplayAbilitySpecHandle UAbilitySystemComponent::GiveAbility(const FGameplayAbilitySpec& Spec) |
函数参数为FGameplayAbilitySpec,表示“这次赋予”的描述信息。
不仅包含这次赋予的描述信息,比如GA的Class,指定GA的等级,GA绑定的输入Tag。而且也是GA在ASC上的runtime表示,有很多runtime信息。每个Spec都有唯一的ID,称为Handle,通过自增赋予,各系统间Spec的传递都通过Handle实现。
值得注意的是Spec.SourceObject属性,表示这个GA所关联的最“密切”的Object,在创建时由我们指定。
比如GA_Weapon_Fire_Pistol,是枪开火的GA,SourceObject就是武器B_WeaponInstance_Pistol;
GA_Hero_Jump是角色跳跃的GA,SourceObejct就是Character;
GA_ShowLeaderboard_TDM则是与战斗无关的打开计分板的GA,OurceObject就是PlayerStat。
1 | USTRUCT(BlueprintType) |
添加时,首先把Spec加入到ActivatableAbilities数组中,然后根据Policy创建一个GA的实例,默认的InstancedPerActor是要创建的。创建完成后,如果GA设置为网络同步,就加入到Spec.ReplicatedInstances中,否则加入Spec.NonReplicatedInstances。
注册Trigger
GA可以绑定一些Trigger,实际是Tag,当发出GamePlayEvent,并指定对应Tag后,就能激活GA了。
所以这里的Tag为了更方便做判断具体的GA。
- 首先是InputTag,它本身非GAS框架内的,是Lyra自己做的一套输入和GA的映射。InputTag在AbilitySet中指定
AbilitySet_ShooterPisto
AbilitySet_ShooterPistol
AbilitySet_ShooterPisto
创建Spec时会把InputTag添加到其DynamicSpecSourceTags,DynamicSpecSourceTags是一个通用Tag容器。
该Tags在结构体FGameplayAbilitySpec里面,也就是说,一个FGameplayAbilitySpec代表的是传递的一个Ability的能力,然后里面有很多DynamicAbilityTags。
注册的路径如下:
1 | void ULyraAbilitySet::GiveToAbilitySystem(ULyraAbilitySystemComponent* LyraASC, FLyraAbilitySet_GrantedHandles* OutGrantedHandles, UObject* SourceObject) const |
在授予ASC Ability的时候,将Tags Effect等信息一并传入。
授予GA的逻辑,以上就是服务器初始化ASC并赋予GA的全部过程。
同步客户端
ASC
首先,ASC作为一个Component挂在PlayerState上,是可以同步的。
由于AvatarActor是Character,因此OwnerActor和AvatarActor的最终设置,需要客户端的Character创建后才能执行。
具体为Character创建后,触发LyraHeroComponent的加载资源流程,当加载完成并且客户端获取到PlayerState后,就把当前阶段设置为DataAvailabe,并最终设置OwnerActor和AvatarActor。
GASpec
Spec存于ActivatableAbilities容器中,本身是FastArray,标记了Replicated,可正常网络同步
1 | UPROPERTY(ReplicatedUsing = OnRep_ActivateAbilities, BlueprintReadOnly, Transient, Category = "Abilities") |
Spec同步到客户端后,FastArray会触发FGameplayAbilitySpec::PostReplicatedAdd()方法,里面再和服务器同样的UAbilitySystemComponent::OnGiveAbility()方法来注册Spec。
1 | void FGameplayAbilitySpec::PostReplicatedAdd(const struct FGameplayAbilitySpecContainer& InArraySerializer) |
对于不同步但InstancedPerActor的GA,会在此时创建实例,加入到NonReplicatedInstances数组,然后再注册AbilityTriggers。
Spec的GA实例
首先,GA实例只同步给Autonomous客户端,不会同步给SimulateProxy。然后,会把GA实例作为ASC的SubObject进行同步,这是SubObject同步是UE自身支持的机制。
主要代码如下:
1 | //同步GA |
同步到客户端后表现如下:
常用调整命令行
ShowDebug AbilitySystem 展示Ability相关的内容
踩坑记录
GAS网络生成的BP需要考虑所有者


如果是在Server端生成的BP,一定要指定“Owner”,它意味着是这个生成的东西在各网络设备之间指定的所有者。
当这个所有者内部需要执行一个网络的Event的时候,它需要通知一个客户端,如果没有Owner,这个Event会被拒绝执行。
随机数问题
注意网络端和服务端生成随机数会不一样。
在GE里拿到“事件触发者”和“事件被影响者”
如上图,我有一个需求,投掷物。
需要给GE传递“投掷物这个物体”和“被撞的人”。
其中,在我这里,“被撞的人”是要执行的ASC的所有者(即这里的Instigator),在应用GE的时候必须提供。
调用 BP_ApplyGameplayEffectToTarget 时,它内部会:
- 用「作为 Target 的 ASC 的 Owner / Avatar Actor」来构造 FGameplayEffectContextHandle;
- 在这个 EffectContext 里自动填好 Instigator、EffectCauser 等字段;
- 把这个 EffectContext 放进 FGameplayEffectSpec,最后应用到 Target;
(正如图上所示,它其实有两个Target节点,上面那个是调用这个事件的Target(蓝图中常见),下面那个是被命中的“被撞的人”,因为碰巧都叫Target所以在这里重复了)
这个比较好理解。但,实际上GC一个内部结构里,有三个储存用来表达“事件责任相关人”的变量:
1 | USTRUCT(BlueprintType, meta = (HasNativeBreak = "/Script/GameplayAbilities.AbilitySystemBlueprintLibrary.BreakGameplayCueParameters", HasNativeMake = "/Script/GameplayAbilities.AbilitySystemBlueprintLibrary.MakeGameplayCueParameters")) |
在文件UE_5.6\Engine\Plugins\Runtime\GameplayAbilities\Source\GameplayAbilities\Public\GameplayEffectTypes.h中
这三个分别是:Instigator、EffectCauser、SourceObject。
按照说明,他们“责任”分别的作用是
- Instigator:谁是「施加这个效果的控制者」,通常是 Pawn/Character。
- EffectCauser:是哪一个 Actor/Component「具体造成了这次效果」,比如武器、投掷物。
- SourceObject:一个完全开放给你用的 UObject*,可以是武器实例、技能实例、DataAsset、投掷物等。
这三个字段的值,不一定都有,是否自动填,取决于你走的 GAS 调用路径。通常应用的路径上,Instigator和EffectCauser都是同一个值,都是这个ASC的拥有者。但是SourceObject通常是空的(通过GA主动和触发的GE有机会把GA的SourceObejct传入进去),所以可以自己指定,用作自己的参数。
参考资料
官方教学视频:我看着这个视频作为最初的入门
关于GA的讲解:别忘了可以点开B站的AI翻译
一个很不错的关于GAS系统实操讲解
文字讲解Lyra结构适合作为研究基础,然后配合上面的视频看。